<

シンプルなアプリ状態管理

さて、あなたはそれについて知っています宣言型 UI プログラミングとの違い一時的なアプリの状態、 簡単なアプリの状態管理について学ぶ準備ができました。

このページでは、providerパッケージ。 Flutter を初めて使用し、選択する強い理由がない場合 別のアプローチ (Redux、Rx、フックなど)、これがおそらくアプローチです から始めるべきです。のproviderパッケージがわかりやすい コードはあまり使用しません。 また、他のすべてのアプローチに適用できる概念も使用します。

そうは言っても、あなたに強力なバックグラウンドがある場合、 他のリアクティブフレームワークからの状態管理、 パッケージとチュートリアルは次の場所にリストされています。オプションページ

私たちの例

An animated gif showing a Flutter app in use. It starts with the user on a login screen. They log in and are taken to the catalog screen, with a list of items. The click on several items, and as they do so, the items are marked as "added". The user clicks on a button and gets taken to the cart view. They see the items there. They go back to the catalog, and the items they bought still show "added". End of animation.

説明のために、次の単純なアプリを考えてみましょう。

アプリには、カタログ、 とカート (で表されます)MyCatalog、 とMyCartそれぞれウィジェット)。ショッピングアプリかもしれませんが、 しかし、単純なソーシャル ネットワーキングでも同じ構造を想像できます。 アプリ (カタログを「ウォール」に、カートを「お気に入り」に置き換えます)。

カタログ画面にはカスタム アプリ バー (MyAppBar) 多くのリスト項目のスクロールビュー (MyListItems)。

これはアプリをウィジェット ツリーとして視覚化したものです。

A widget tree with MyApp at the top, and  MyCatalog and MyCart below it. MyCart area leaf nodes, but MyCatalog have two children: MyAppBar and a list of MyListItems.

したがって、少なくとも 5 つのサブクラスがあります。1036753a-d8c4-415a-be8e-8bbbbf585336。彼らの多くは必要とする 他の場所に「属する」状態へのアクセス。たとえば、それぞれMyListItem自身をカートに追加できる必要があります。 また、現在表示されている項目が はすでにカートに入っています。

これにより、最初の質問が始まります。現在のものをどこに置くべきかということです。 カートの状態は?

状態を引き上げる

flutterでは、 状態を使用するウィジェットの上に状態を保持することは理にかなっています。

なぜ? Flutterのような宣言型フレームワークでUIを変更したい場合は、 それを再構築する必要があります。簡単に手に入れる方法はないMyCart.updateWith(somethingNew)。言い換えれば、難しいのは、 ウィジェットのメソッドを呼び出すことで、外部からウィジェットを強制的に変更します。 たとえこれがうまくいったとしても、あなたは敵と戦うことになるでしょう。 フレームワークを役立つようにするのではなく、フレームワークを使用します。

// BAD: DO NOT DO THIS
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

上記のコードが動作したとしても、 そうすればあなたは対処しなければならないでしょう に次のものがありますMyCartウィジェット:

// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
  return SomeWidget(
    // The initial state of the cart.
  );
}

void updateWith(Item item) {
  // Somehow you need to change the UI from here.
}

UI の現在の状態を考慮する必要があります。 新しいデータをそれに適用します。この方法でバグを回避するのは困難です。

Flutter では、内容が変更されるたびに新しいウィジェットを構築します。 それ以外のMyCart.updateWith(somethingNew)(メソッド呼び出し) あなたが使うMyCart(contents)(コンストラクター)。あなたにしかできないから 親のビルドメソッドで新しいウィジェットを構築します。 変わりたいならcontentsに住む必要があります。MyCartの 親以上。

// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}

MyCartには、任意のバージョンの UI を構築するためのコード パスが 1 つだけあります。

// GOOD
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // Just construct the UI once, using the current state of the cart.
    // ···
  );
}

私たちの例では、contentsに住む必要があるMyApp。変化するたびに、 それは再構築しますMyCart上から(詳細は後ほど)。このため、MyCartライフサイクルを気にする必要はありません。宣言するだけです。 与えられたものに対して何を示すかcontents。それが変わると、古いものはMyCartウィジェットが消え、新しいウィジェットに完全に置き換えられます。

Same widget tree as above, but now we show a small 'cart' badge next to MyApp, and there are two arrows here. One comes from one of the MyListItems to the 'cart', and another one goes from the 'cart' to the MyCart widget.

これが、ウィジェットが不変であると言うときの意味です。 それらは変わるのではなく、置き換えられるのです。

カートの状態をどこに置くかがわかったので、その方法を見てみましょう。 アクセスするには。

状態へのアクセス

ユーザーがカタログ内のアイテムの 1 つをクリックすると、 カートに追加されます。でもカートは上にあるのでMyListItem、 どうすればいいでしょうか?

簡単なオプションは、次のようなコールバックを提供することです。MyListItem呼び出すことができます クリックされたとき。 Dart の関数はファーストクラスのオブジェクトです。 好きなように渡すことができます。それで、中ではMyCatalog次のように定義できます。

@override
Widget build(BuildContext context) {
  return SomeWidget(
    // Construct the widget, passing it a reference to the method above.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}

これは問題なく動作しますが、アプリの状態を変更する必要がある場合は、 さまざまな場所にあるため、多くの場所を通過する必要があります コールバック - これはすぐに古くなってしまいます。

幸いなことに、Flutter にはウィジェットがデータを提供するメカニズムがあり、 子孫への奉仕(言い換えれば、子供たちだけでなく、 ただし、その下のウィジェットはすべて含まれます)。ご想像のとおり、Flutter では、 どこすべてはウィジェット™、これらのメカニズムは単に特別です ウィジェットの種類—InheritedWidgetInheritedNotifierInheritedModel、 もっと。ここではそれらについては取り上げませんが、 なぜなら、それらは私たちがやろうとしていることに対して少し低レベルだからです。

代わりに、低レベルで動作するパッケージを使用します。 ウィジェットですが使い方は簡単です。それは呼ばれていますprovider

作業する前にprovider、 それへの依存関係を忘れずに追加してくださいpubspec.yaml

追加するには、providerパッケージを依存関係として実行しますflutter pub add:

$ flutter pub add provider

今ならできるimport 'package:provider/provider.dart';そして構築を開始します。

provider、コールバックやInheritedWidgets。ただし、次の 3 つの概念を理解する必要があります。

  • ChangeNotifier
  • ChangeNotifierProvider
  • 消費者

ChangeNotifier

ChangeNotifierFlutter SDK に含まれる単純なクラスです。 リスナーへの変更通知。言い換えれば、何かがあれば、 あるChangeNotifier、その変更を購読できます。 (の形です この用語に精通している人にとっては観察可能です。)

providerChangeNotifierアプリケーションをカプセル化する 1 つの方法です 州。非常に単純なアプリの場合は、1 つだけで済みます。ChangeNotifier。 複雑なモデルでは、複数のモデルが存在するため、複数のモデルが存在します。ChangeNotifiers。 (使用する必要はありませんChangeNotifierprovider全く問題ありませんが、扱いやすいクラスです。)

ショッピング アプリの例では、カートの状態を管理したいと考えています。ChangeNotifier。次のように、それを拡張する新しいクラスを作成します。

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

に固有の唯一のコードChangeNotifier電話です にnotifyListeners()。モデルが何らかの方法で変更されるたびにこのメソッドを呼び出します アプリの UI が変更される可能性があります。それ以外はすべてCartModelそれは モデル自体とそのビジネス ロジック。

ChangeNotifierの一部ですflutter:foundationそして依存しない Flutter の上位レベルのクラス。簡単にテストできます(テストする必要すらありません) 使用するウィジェットのテストそれのための)。例えば、 これは簡単な単体テストですCartModel:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  var i = 0;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
    i++;
  });
  cart.add(Item('Dash'));
  expect(i, 1);
});

ChangeNotifierProvider

ChangeNotifierProviderのインスタンスを提供するウィジェットです あるChangeNotifierその子孫に。それはから来ていますproviderパッケージ。

どこに置くかはすでにわかっていますChangeNotifierProvider: ウィジェットの上にある それにアクセスする必要があります。の場合CartModel、それはどこかを意味します 両方の上にMyCartMyCatalog

置きたくないChangeNotifierProvider必要以上に高い (スコープを汚したくないため)。しかし、私たちの場合、 両方の上にある唯一のウィジェットMyCartMyCatalogMyApp

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

新しいインスタンスを作成するビルダーを定義していることに注意してください。 のCartModelChangeNotifierProvider十分賢いですいいえ再構築するCartModel絶対に必要な場合を除きます。また、自動的に電話をかけますdispose()の上CartModelインスタンスが不要になったとき。

複数のクラスを提供したい場合は、次のように使用できます。MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}

消費者

CartModelを通じてアプリ内のウィジェットに提供されます。ChangeNotifierProvider上部で宣言すると、使用を開始できます。

これは、Consumerウィジェット。

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text('Total price: ${cart.totalPrice}');
  },
);

アクセスしたいモデルのタイプを指定する必要があります。 この場合、私たちが望むのは、CartModel、ということで書きますConsumer<CartModel>。ジェネリックを指定しない場合 (<CartModel>)、 のproviderパッケージはあなたを助けることができません。providerタイプに基づいており、 型がなければ、ユーザーが何を望んでいるのかわかりません。

の唯一の必須引数Consumerウィジェット ビルダーです。 Builder は、ChangeNotifier変化します。 (つまり、電話をかけると、notifyListeners()モデル内の、対応するすべてのビルダー メソッドConsumerウィジェットが呼び出されます。)

ビルダーは 3 つの引数を指定して呼び出されます。一つ目はcontext、 これはすべてのビルド メソッドでも得られます。

ビルダー関数の 2 番目の引数は、次のインスタンスです。 のChangeNotifier。それが私たちが最初に求めていたものです。 モデル内のデータを使用して、UI がどのように見えるかを定義できます。 いつでも。

3 番目の引数はchild、最適化のためにあります。 大きなウィジェットのサブツリーが下にある場合は、Consumerそれかしませんモデルが変更されると変更され、それを構築できます 一度ビルダーを通して取得してください。

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text('Total price: ${cart.totalPrice}'),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

ベストプラクティスとして、Consumerツリーの奥深くにあるウィジェット できるだけ。 UI の大部分を再構築したくない場合 どこかの詳細が変更されただけです。

// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

その代わり:

// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);

プロバイダーの

場合によっては、実際には必要ない場合もありますデータ変更するモデル内で UI ですが、アクセスする必要があります。たとえば、ClearCartボタンは、ユーザーがカートからすべてを削除できるようにしたいと考えています。 カートの中身を表示する必要はありませんが、 を呼び出すだけですclear()方法。

使えますConsumer<CartModel>このため、 しかしそれは無駄です。私たちがフレームワークに求めるのは、 再構築する必要のないウィジェットを再構築します。

このユースケースでは、次を使用できますProvider.of、 とともにlistenパラメータを次のように設定false

Provider.of<CartModel>(context, listen: false).removeAll();

ビルド メソッドで上記の行を使用しても、このウィジェットは いつ再構築するかnotifyListenersと呼ばれます。

すべてを一緒に入れて

あなたはできる例をチェックしてくださいこの記事で説明します。 もっとシンプルなものが必要な場合は、 シンプルなカウンター アプリがどのように表示されるかを確認してください。で構築されたprovider

これらの記事に従うことで、大幅な効果が得られます 状態ベースのアプリケーションを作成する能力が向上しました。 でアプリケーションを構築してみてくださいprovider自分自身に これらのスキルをマスターしてください。